package cmd

import (
	"bytes"
	"context"
	"errors"
	"golang.org/x/text/encoding/simplifiedchinese"
	"io"
	"io/ioutil"
	"os/exec"
	"runtime"
	"syscall"
	"time"
)

type Cmd struct {
	cmd    *exec.Cmd
	out    io.ReadCloser
	ctx    context.Context
	cancel context.CancelFunc
}

func (c *Cmd) init() {
	c.ctx, c.cancel = context.WithCancel(context.Background())
	c.cmd = exec.Command("cmd")
	c.out, _ = c.cmd.StdoutPipe()

	if runtime.GOOS == "windows" {
		c.cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
	}
}

func (c *Cmd) close() {
	c.cancel()
	c.out.Close()
	c.cmd.Process.Kill()
	c.cmd.Process.Release()
}

var ErrTimeout = errors.New("exec commond timeout")

func (c *Cmd) timeout(duration time.Duration) {
	go func() {
		t := time.After(duration)
		select {
		case <-c.ctx.Done():
			return
		case <-t:
			c.close()
		}
	}()
}

// start command
// when the timeout is set to zero,it will wait indefinitely
// command can be multiple
func (c *Cmd) Start(timeout time.Duration, command ...string) (output string, err error) {
	c.init()
	defer c.close()

	if len(command) == 0 {
		return "", nil
	}

	in := bytes.NewBuffer(nil)
	c.cmd.Stdin = in

	for _, v := range command {
		in.WriteString(v + "\n")
	}

	if err = c.cmd.Start(); err != nil {
		return "", err
	}

	if timeout != 0 {
		c.timeout(timeout)
	}

	outCh := c.output()

	if err = c.cmd.Wait(); err != nil {
		if err.Error() == "exit status 1" {
			err = ErrTimeout
		}
	} else {
		c.cancel()
	}
	output = <-outCh
	close(outCh)
	return
}

type Coding string

const (
	GBK      = Coding("GBK")
	HZGB2312 = Coding("HZGB2312")
	GB18030  = Coding("GB18030")
	UTF8     = Coding("UTF8")
)

// start command
// when the timeout is set to zero,it will wait indefinitely
// command can be multiple
// automaticlly stops when a keyword is checked
func (c *Cmd) StartWithKeyword(timeout time.Duration, coding Coding, keyworld string, command ...string) (seekOut bool, err error) {
	if len(command) == 0 {
		return
	}
	c.init()
	defer c.close()

	in := bytes.NewBuffer(nil)
	c.cmd.Stdin = in

	for _, v := range command {
		in.WriteString(v + "\n")
	}

	if err = c.cmd.Start(); err != nil {
		return
	}

	if timeout != 0 {
		c.timeout(timeout)
	}

	// keyword check
	go func() {
		var (
			i        int
			resBytes []byte
			search   []byte
		)

		switch coding {
		case HZGB2312:
			search, _ = simplifiedchinese.HZGB2312.NewEncoder().Bytes([]byte(keyworld))
		case GBK:
			search, _ = simplifiedchinese.GBK.NewEncoder().Bytes([]byte(keyworld))
		case GB18030:
			search, _ = simplifiedchinese.GB18030.NewEncoder().Bytes([]byte(keyworld))
		default:
			search = []byte(keyworld)
		}
		for {
			temp := make([]byte, len(search))
			i, err = c.out.Read(temp)
			if err != nil {
				return
			}
			resBytes = append(resBytes, temp[:i]...)
			if bytes.LastIndex(resBytes, search) != -1 {
				seekOut = true
				c.close()
				return
			}
		}
	}()

	if err = c.cmd.Wait(); err != nil {
		if seekOut {
			err = nil
			return
		}
		switch err.Error() {
		case "GetExitCodeProcess: The handle is invalid.":
			err = ErrTimeout
		}
	}
	return
}

func (c *Cmd) output() chan string {
	ch := make(chan string)
	go func() {
		defer recover()

		res, _ := ioutil.ReadAll(c.out)
		res, _ = simplifiedchinese.GB18030.NewDecoder().Bytes(res)
		ch <- string(res)
	}()
	return ch
}

// ====================================================================================================================

var cmd Cmd

func Start(duration time.Duration, command ...string) (output string, err error) {
	return cmd.Start(duration, command...)
}
func StartWithKeyword(timeout time.Duration, coding Coding, keyworld string, command ...string) (seekOut bool, err error) {
	return cmd.StartWithKeyword(timeout, coding, keyworld, command...)
}
